﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Tnelab.Common
{
    public class ContextSerializerDelegateClearProxy
    {
        public Action? ClearAction { get; set; }
        ~ContextSerializerDelegateClearProxy()
        {
            if (ClearAction != null)
                ClearAction();
        }
    }
    public class ContextSerializerDelegateInvokeInfo
    {
        public string? FunctionPath { get; internal set; }
        public Type? ReturnType { get; internal set; }
        public object[]? Args { get; internal set; }
        public ContextSerializerDelegateClearProxy? ClearProxy { get; set; }
    }
    public class ContextSerializer
    {
        readonly string NULL_FLAG = "THISISNULL";        
        protected Func<ContextSerializerDelegateInvokeInfo,object>? DelegateImplementation { get; private set; }
        public void SetDelegateImplementation(Func<ContextSerializerDelegateInvokeInfo, object> delegateImplementation)
        {
            this.DelegateImplementation = delegateImplementation;
        }

        public void ToBytes(object obj, BinaryWriter bw)
        {
            GetBytes(obj, bw);
        }
        public object FromBytes(Type type, BinaryReader br)
        {
            object robj = CreateByBytes(type, br);
            return robj;
        }
        public int GetTypeSize(string typeName)
        {
            return typeName switch
            {
                "System.Byte" => 1,
                "System.SByte" => 1,
                "System.UInt16" => 2,
                "System.Int16" => 2,
                "System.UInt32" => 4,
                "System.Int32" => 4,
                "System.UInt64" => 8,
                "System.Int64" => 8,
                "System.Single" => 4,
                "System.Double" => 8,
                _ => throw new NotSupportedException()
            };
        }
        #region GetBytes
        void GetBytes(object obj, BinaryWriter bw)
        {
            if (obj == null)
            {
                bw.Write(Encoding.UTF8.GetBytes(NULL_FLAG));
                return;
            }
            var type = obj.GetType();
            if (type.IsArray)
            {
                GetBytesForArray(obj, bw);
            }
            else if (type.FullName.StartsWith("System.Collections.Generic.List"))
            {
                GetBytesForList(obj, bw);
            }
            else if (type == typeof(string))
            {
                GetBytesForString((string)obj, bw);
            }
            else
            {
                if (!GetBytesForNumber(obj, bw))
                {
                    GetBytesForEntity(obj, bw);
                }
            }
        }
        byte[] Sbytes2Bytes(sbyte[] sbytes)
        {
            var bytes = new byte[sbytes.Length];
            Buffer.BlockCopy(sbytes, 0, bytes, 0, sbytes.Length);
            return bytes;
        }
        bool GetBytesForNumber(object obj, BinaryWriter bw)
        {
            var typeName = obj.GetType().FullName;
            var bytes = typeName switch
            {
                "System.Byte" => new byte[1] { (byte)obj },
                "System.SByte" => Sbytes2Bytes(new sbyte[1] { (sbyte)obj }),
                "System.UInt16" => BitConverter.GetBytes((System.UInt16)obj),
                "System.Int16" => BitConverter.GetBytes((System.Int16)obj),
                "System.UInt32" => BitConverter.GetBytes((System.UInt32)obj),
                "System.Int32" => BitConverter.GetBytes((System.Int32)obj),
                "System.UInt64" => BitConverter.GetBytes((System.UInt64)obj),
                "System.Int64" => BitConverter.GetBytes((System.Int64)obj),
                "System.Single" => BitConverter.GetBytes((System.Single)obj),
                "System.Double" => BitConverter.GetBytes((System.Double)obj),
                "System.Boolean" => BitConverter.GetBytes((System.Boolean)obj),
                _ => new byte[0]
            };
            bw.Write(bytes);
            return bytes.Length != 0;
        }
        void GetBytesForString(string obj, BinaryWriter bw)
        {
            var bytes = Encoding.UTF8.GetBytes(obj);
            bw.Write((BitConverter.GetBytes(bytes.Length)));
            bw.Write(bytes);
        }
        void GetBytesForArray(dynamic objs, BinaryWriter bw)
        {
            var bs = objs as byte[];
            bw.Write(BitConverter.GetBytes(objs.Length));
            if (bs != null)
            {
                bw.Write(bs);
                return;
            }
            var len = objs.Length;
            for (var i = 0; i < len; i++)
            {
                GetBytes(objs[i], bw);
            }
        }
        void GetBytesForList(dynamic objs, BinaryWriter bw)
        {
            List<byte> list = objs as List<byte>;
            bw.Write(BitConverter.GetBytes(objs.Count));
            if (list != null)
            {
                bw.Write(list.ToArray());
                return;
            }
            var count = objs.Count;
            for (var i = 0; i < count; i++)
            {
                GetBytes(objs[i], bw);
            }
        }
        void GetBytesForEntity(object obj, BinaryWriter bw)
        {
            var type = obj.GetType();
            var props = type.GetProperties();
            foreach (var prop in props)
            {
                GetBytes(prop.GetValue(obj), bw);
            }
        }
        #endregion
        object _DelegateImplementation(params object[] objs)
        {
            if (DelegateImplementation != null)
            {
                var functionPath = objs[0].ToString();
                var returnType = objs[1] as Type;
                var objForClear = objs[2] as ContextSerializerDelegateClearProxy;
                var args = new object[objs.Length - 3];
                for (var i = 0; i < args.Length; i++)
                {
                    args[i] = objs[i + 3];
                }
                var callInfo = new ContextSerializerDelegateInvokeInfo();
                callInfo.FunctionPath = functionPath;
                callInfo.ReturnType = returnType;
                callInfo.Args = args;
                callInfo.ClearProxy = objForClear;
                var result=DelegateImplementation(callInfo);
                return result;
            }
            return null;
        }
        static MethodInfo _DelegateImplementationMethod = typeof(ContextSerializer).GetMethod("_DelegateImplementation", BindingFlags.NonPublic | BindingFlags.Instance);
        object GetDelegate(string FunctionPath, Type type)
        {
            //if (CallbackDelegateDic.ContainsKey(FunctionPath))
            //{
            //    return CallbackDelegateDic[FunctionPath];
            //}
            object result = null;

            var delegateInfo = type.GetMethod("Invoke");
            bool theIsAction = delegateInfo.ReturnType == typeof(void);
            var paramTypes = delegateInfo.GetParameters().Select(it => it.ParameterType).ToArray();

            List<ParameterExpression> paramExps = new List<ParameterExpression>();
            List<Expression> vParamExps = new List<Expression>() {
                Expression.Convert(Expression.Constant(FunctionPath), typeof(object)),
                Expression.Convert(Expression.Constant(delegateInfo.ReturnType),typeof(object)),
                Expression.Convert(Expression.Constant(new ContextSerializerDelegateClearProxy()),typeof(object))
            };
            var len = paramTypes.Length;
            for (var i = 0; i < len; i++)
            {
                var pt = paramTypes[i];
                var pEx = Expression.Parameter(pt);
                paramExps.Add(pEx);
                vParamExps.Add(Expression.Convert(pEx, typeof(object)));
            }
            var arrayExp = Expression.NewArrayInit(typeof(object), vParamExps);
            LambdaExpression lambdaExpr;
            var thisExpression = Expression.Constant(this);
            if (theIsAction)
            {
                lambdaExpr = Expression.Lambda(type, Expression.Call(thisExpression, _DelegateImplementationMethod, arrayExp), paramExps);
            }
            else
            {
                lambdaExpr = Expression.Lambda(type, Expression.Convert(Expression.Call(thisExpression, _DelegateImplementationMethod, arrayExp), delegateInfo.ReturnType), paramExps);
            }
            result = lambdaExpr.Compile();
            return result;
        }
        object CreateByBytes(Type type, BinaryReader br)
        {
            if (br.BaseStream.Length - br.BaseStream.Position >= NULL_FLAG.Length)
            {
                var nullBytes = br.ReadBytes(NULL_FLAG.Length);
                if (Encoding.UTF8.GetString(nullBytes) == NULL_FLAG)
                {
                    return null;
                }
                br.BaseStream.Position -= NULL_FLAG.Length;
            }
            object obj;
            if (type.FullName.StartsWith("System.Action") || type.FullName.StartsWith("System.Func`"))
            {
                var FunctionPath = CreateByBytes(typeof(string), br);
                return GetDelegate(FunctionPath as string, type);
            }
            if (type.IsArray)
            {
                obj = FromBytesForArray(type, br);
            }
            else if (type.FullName.StartsWith("System.Collections.Generic.List"))
            {
                obj = FromBytesForList(type, br);
            }
            else if (type == typeof(string))
            {
                obj = FromBytesForString(type, br);
            }
            else
            {
                obj = FromBytesForNumber(type, br);
                if (obj == null)
                {
                    obj = FromBytesForEntity(type, br);
                }
            }
            return obj;
        }
        object FromBytesForNumber(Type type, BinaryReader br)
        {
            object obj = type.FullName switch
            {
                "System.Byte" => br.ReadByte(),
                "System.SByte" => (sbyte)br.ReadSByte(),
                "System.UInt16" => br.ReadUInt16(),
                "System.Int16" => br.ReadInt16(),
                "System.UInt32" => br.ReadUInt32(),
                "System.Int32" => br.ReadInt32(),
                "System.UInt64" => br.ReadUInt64(),
                "System.Int64" => br.ReadInt64(),
                "System.Single" => br.ReadSingle(),
                "System.Double" => br.ReadDouble(),
                "System.Boolean" => br.ReadBoolean(),
                _ => null
            };
            return obj;
        }
        object FromBytesForString(Type type, BinaryReader br)
        {
            object obj;
            var len = br.ReadInt32();
            var stringBytes = br.ReadBytes(len);
            obj = Encoding.UTF8.GetString(stringBytes);
            return obj;
        }
        object FromBytesForEntity(Type type, BinaryReader br)
        {
            object obj = Activator.CreateInstance(type);
            var props = type.GetProperties();
            foreach (var prop in props)
            {
                prop.SetValue(obj, CreateByBytes(prop.PropertyType, br));
            }
            return obj;
        }
        object FromBytesForArray(Type type, BinaryReader br)
        {
            var len = br.ReadInt32();
            if (type == typeof(byte[]))
            {
                return br.ReadBytes(len);
            }
            dynamic objs = Activator.CreateInstance(type, len);
            var subType = type.Assembly.GetType(type.FullName.Replace("[]", ""));
            for (var i = 0; i < len; i++)
            {
                dynamic obj = CreateByBytes(subType, br);
                objs[i] = obj;
            }
            return objs;
        }
        object FromBytesForList(Type type, BinaryReader br)
        {
            var len = br.ReadInt32();
            if (type == typeof(List<byte>))
            {
                return new List<byte>(br.ReadBytes(len));
            }
            dynamic objs = Activator.CreateInstance(type);
            var subType = type.GetGenericArguments()[0];
            for (var i = 0; i < len; i++)
            {
                dynamic obj = CreateByBytes(subType, br);
                objs.Add(obj);
            }
            return objs;
        }
    }
}
